package org.third.integration.ldap.ldaps;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Hashtable;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.third.common.utils.StreamUtils;
import org.third.common.utils.StringUtils;

/**
 * Creates LDAP directory contexts for performing operations on LDAP domains
 */
public class LdapContextFactory {
	private static final Logger logger = LoggerFactory.getLogger(LdapContextFactory.class);
	/** Key for LDAP context environment hashtable to indicate the read timeout */
	public static final String LdapReadTimeoutKey = "com.sun.jndi.ldap.read.timeout";
	/**
	 * Key for LDAP context environment hashtable to indicate the connect timeout
	 */
	public static final String LdapConnectTimeoutKey = "com.sun.jndi.ldap.connect.timeout";

	/** Default value for LDAP read timeout: 30 seconds */
	public static final int DefaultReadTimeout = 30000;
	/** Value for LDAP read timeout (in milliseconds) */
	private int readTimeout = DefaultReadTimeout;
	/** Default value for LDAP connect timeout: 30 seconds */
	public static final int DefaultConnectTimeout = 30000;
	/** Value for LDAP read timeout (in milliseconds) */
	private int connectTimeout = DefaultConnectTimeout;
	/** LDAPS trust store resource, password, type */
	private Resource trustStoreResource;
	private String trustStorePassword;
	private String trustStoreType;

	private KeyStore trustStore;

	/**
	 * Creates an LDAP context using an associative array of LDAP connection
	 * properties
	 *
	 * @param env
	 *            the associative array of LDAP connection properties
	 *
	 * @return an LDAP directory context for performing LDAP operations
	 *
	 * @throws NamingException
	 *             if an error occurs, generally with transport user credentials
	 *             when accessing a server that does not allow anonymous bind.
	 */
	@SuppressWarnings("all")
	public LdapContext createContext(Hashtable<?, ?> env) throws NamingException {
		return new InitialLdapContext(env, null);
	}

	public Hashtable<String, String> setupBasicEnvironments() {
		Hashtable<String, String> env = new Hashtable<>();
		env.put(LdapReadTimeoutKey, getReadTimeoutValue());
		env.put(LdapConnectTimeoutKey, getConnectTimeoutValue());
		env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
		KeyStore trustStore = getTrustStore();
		if (trustStore != null) {
			env.put("java.naming.ldap.factory.socket", LdapsSocketFactory.class.getName());
			LdapsSocketFactory.setTrustStore(trustStore);
		}
		return env;
	}

	public KeyStore getTrustStore() {
		if (trustStore != null)
			return trustStore;
		InputStream inputStream = null;
		try {
			if (trustStoreResource == null)
				return null;
			inputStream = trustStoreResource.getInputStream();
			String type = StringUtils.hasText(trustStoreType) ? trustStoreType : KeyStore.getDefaultType();
			trustStore = KeyStore.getInstance(type);
			char[] password = null;
			if (trustStorePassword != null && trustStorePassword.length() != 0) {
				password = trustStorePassword.toCharArray();
			}
			trustStore.load(inputStream, password);
			return trustStore;
		} catch (KeyStoreException e) {
			logger.error("KeyStore error with " + trustStoreResource, e);
		} catch (CertificateException e) {
			logger.error("Certificate error with" + trustStoreResource, e);
		} catch (NoSuchAlgorithmException e) {
			logger.error("NoSuchAlgorithmException with " + trustStoreResource, e);
		} catch (IOException e) {
			logger.error("IOException with " + trustStoreResource, e);
		} finally {
			StreamUtils.closeQuietly((Closeable) inputStream);
		}
		trustStore = null;
		return null;
	}

	/**
	 * Gets the read timeout for an LDAP operation
	 *
	 * @return the read timeout for an LDAP operation, in milliseconds
	 */
	public int getReadTimeout() {
		return this.readTimeout;
	}

	/**
	 * Gets the read timeout for an LDAP operation
	 *
	 * @return the read timeout for an LDAP operation
	 */
	public String getReadTimeoutValue() {
		return Integer.toString(this.readTimeout);
	}

	/**
	 * Sets the read timeout for an LDAP operation
	 *
	 * @param timeout
	 *            the read timeout for an LDAP operation
	 */
	public void setReadTimeout(int timeout) {
		this.readTimeout = timeout;
	}

	/**
	 * Gets the connect timeout for an LDAP operation
	 *
	 * @return the connect timeout for an LDAP operation, in milliseconds
	 */
	public int getConnectTimeout() {
		return this.connectTimeout;
	}

	/**
	 * Gets the connect timeout for an LDAP operation
	 *
	 * @return the connect timeout for an LDAP operation
	 */
	public String getConnectTimeoutValue() {
		return Integer.toString(this.connectTimeout);
	}

	/**
	 * Sets the connect timeout for an LDAP operation
	 *
	 * @param timeout
	 *            the read timeout for an LDAP operation
	 */
	public void setConnectTimeout(int timeout) {
		this.connectTimeout = timeout;
	}

	/** Flag indicating whether valid hostname are required */
	private boolean requireValidHostname = false;

	/**
	 * Gets a flag indicating whether valid hostname are required.
	 *
	 * @return {@code true} if valid hostname are required; {@code false} otherwise.
	 */
	public boolean getRequireValidHostname() {
		return this.requireValidHostname;
	}

	/**
	 * Sets a flag indicating whether valid hostname are required.
	 *
	 * @param requireValidHostname
	 *            {@code true} if valid hostname are required; {@code false}
	 *            otherwise.
	 */
	public void setRequireValidHostname(boolean requireValidHostname) {
		this.requireValidHostname = requireValidHostname;
	}

	/**
	 * Gets LDAPS CA certificates path
	 * 
	 * @return
	 */
	public Resource getTrustStoreResource() {
		return trustStoreResource;
	}

	/**
	 * Sets LDAPS CA certificates path
	 * 
	 * @param trustStoreResource
	 */
	public void setTrustStoreResource(Resource trustStoreResource) {
		this.trustStoreResource = trustStoreResource;
	}

	public String getTrustStorePassword() {
		return trustStorePassword;
	}

	public void setTrustStorePassword(String trustStorePassword) {
		this.trustStorePassword = trustStorePassword;
	}

	public String getTrustStoreType() {
		return trustStoreType;
	}

	public void setTrustStoreType(String trustStoreType) {
		this.trustStoreType = trustStoreType;
	}
}
